package xml;

import util.FileUtil;

import java.io.*;

/**
 * Instances of this class each represent the DOM of an XML file.
 *
 * @author Neil Dickson
 */
public class XML {
	private	File	file;
	private	Tag		root;

	public XML(File file) throws IOException {
		this(file,false,false);
	}
	public XML(String xml,boolean ignoreText) throws IOException {
		int rootStart = findNextTag(xml,0);
		if (rootStart==-1) {
			throw new ParseException("The XML file "+((file!=null)?("\""+file.getPath()+"\" "):"")+"has no root element.");
		}
		root = parseTag(xml,new int[]{rootStart},false,false,ignoreText);
	}
	public XML(File file,boolean trimWhitespace,boolean minimizeWhitespace) throws IOException {
		this(FileUtil.readFileToString(file),trimWhitespace,minimizeWhitespace);
		this.file = file;
	}
	public XML(String xml,boolean trimWhitespace,boolean minimizeWhitespace) throws IOException {
		int rootStart = findNextTag(xml,0);
		if (rootStart==-1) {
			throw new ParseException("The XML file "+((file!=null)?("\""+file.getPath()+"\" "):"")+"has no root element.");
		}
		root = parseTag(xml,new int[]{rootStart},trimWhitespace,minimizeWhitespace,false);
	}
	public XML(Tag root) {
		this.file = null;
		this.root = root;
	}
	private int findNextTag(String xml,int start) {
		int i=start;
		while (true) {
			while (i<xml.length() && xml.charAt(i)!='<') {
				++i;
			}
			if (i+2<xml.length()) {
				if (xml.charAt(i+1)=='?') {
					i+=3;
					while (i<xml.length() && (xml.charAt(i-1)!='?' || xml.charAt(i)!='>')) {
						++i;
					}
				}
				else if (xml.charAt(i+1)=='!') {
					i+=2;
					while (i<xml.length() && xml.charAt(i)!='>') {
						++i;
					}
				}
				else if (xml.charAt(i+1)<=' ') {
					return -1;
				}
				else {
					return i+1;
				}
			}
			else {
				return -1;
			}
		}
	}
	private Tag parseTag(String xml,int[] start,boolean trimWhitespace,boolean minimizeWhitespace,boolean ignoreText) throws ParseException {
		if (xml.charAt(start[0])=='/') {
			return null;
		}
		int i=start[0]+1;
		while (i<xml.length() && xml.charAt(i)>' ' && xml.charAt(i)!='>' && xml.charAt(i)!='/') {
			++i;
		}
		if (i>=xml.length()) {
			throw new ParseException("The XML ends in the middle of a tag.");
		}
		String name = xml.substring(start[0],i);
		Tag tag = new Tag(name);

		while (true) {
			while (i<xml.length() && xml.charAt(i)<=' ') {
				++i;
			}
			if (i>=xml.length()) {
				throw new ParseException("The XML ends in the middle of a "+name+" tag.");
			}

			if (xml.charAt(i)=='/' || xml.charAt(i)=='>') {
				break;
			}

			int attributeStart = i;
			while (i<xml.length() && xml.charAt(i)!='=' && xml.charAt(i)>' ' && xml.charAt(i)!='/' && xml.charAt(i)!='>') {
				++i;
			}
			// +3 is for that there must be at least =""> and the = character is at i
			if (i+3>=xml.length()) {
				throw new ParseException("The XML ends in the middle of a "+name+" tag.");
			}
			if (xml.charAt(i)!='=') {
				throw new ParseException("The XML contains an attribute name that is not followed by \'=\' in a "+name+" tag.");
			}
			if (xml.charAt(i+1)!='"') {
				throw new ParseException("The XML contains an attribute without quotation marks in a "+name+" tag.");
			}
			String attributeName = xml.substring(attributeStart,i);
			i+=2;
			int attributeValueStart = i;
			while (i<xml.length() && xml.charAt(i)!='"') {
				++i;
			}
			// +1 is for that there must be at least "> and the " character is at i
			if (i+1>=xml.length()) {
				throw new ParseException("The XML ends in the middle of a "+name+" tag.");
			}
			String attributeValue = unescape(xml.substring(attributeValueStart,i));
			tag.addAttribute(attributeName,attributeValue);
			++i;
		}
		tag.setSelfClosing(xml.charAt(i)=='/');
		if (xml.charAt(i)=='/') {
			++i;
			if (i>=xml.length()) {
				throw new ParseException("The XML ends in the middle of a "+name+" tag.");
			}
			if (xml.charAt(i)!='>') {
				throw new ParseException("The XML has a "+name+" tag with a '/' that is not followed by a '>'");
			}
			++i;
		}
		else {
			++i;
			int nextTagOffset;
			while ((nextTagOffset = findNextTag(xml,i))>=0) {
				if (!ignoreText && nextTagOffset>i+1) {
					String text = xml.substring(i,nextTagOffset-1);
					if (trimWhitespace) {
						text = text.trim();
					}
					if (minimizeWhitespace) {
						text = text.replace("\\s\\s+"," ");
					}
					if (text.length()>0) {
						tag.addChild(new Text(unescape(text)));
					}
				}
				if (xml.charAt(nextTagOffset)=='/') {
					if (!xml.startsWith(name+">",nextTagOffset+1)) {
						throw new ParseException("The XML has a "+name+" tag with a mismatched closing tag");
					}
					break;
				}
				int[] end = new int[]{nextTagOffset};
				tag.addChild(parseTag(xml,end,trimWhitespace,minimizeWhitespace,ignoreText));
				i = end[0];
			}

			if (nextTagOffset<0) {
				throw new ParseException("The XML has a "+name+" tag with no associated closing tag");
			}
			// Place i after the </name>
			i=nextTagOffset+1+name.length()+1;
		}
		start[0] = i;
		return tag;
	}

	public static String unescape(String text) {
		text = text.replace("&quot;","\"").replace("&apos;","'").replace("&lt;","<").replace("&gt;",">");

		int i;
		while ((i = text.indexOf("&#"))>=0) {
			i+=2;
			int start = i;
			while (i<text.length() && text.charAt(i)!=';') {
				++i;
			}
			try {
				int value = Integer.parseInt(text.substring(start,i));
				if (value>0 && value<=0xFFFF) {
					text = text.substring(0,start-2)+((char)value);
				}
				else {
					text = text.substring(0,start-2)+"&amp;"+text.substring(start-1);
				}
			}
			catch (NumberFormatException e) {
				text = text.substring(0,start-2)+"&amp;"+text.substring(start-1);
			}
		}

		return text.replace("&amp;","&");
	}
	public static String escape(String text) {
		text = text.replace("&","&amp;");

		text = text.replace("\"","&quot;").replace("'","&apos;").replace("<","&lt;").replace(">","&gt;");

		for (int i=0;i<text.length();++i) {
			if (text.charAt(i)>0x7F) {
				text = text.substring(0,i)+"&#"+((int)text.charAt(i))+";"+text.substring(i+1);
			}
		}
		return text;
	}

	public Tag getRoot() {
		return root;
	}

	public String toString() {
		return "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"+root.toString();
	}

	public void save() throws IOException {
		save(file);
	}

	public void save(File file) throws IOException {
		String text = toString();
		PrintStream output = new PrintStream(file);
		output.print(text);
		output.close();
	}

	public static class ParseException extends IOException {
		public ParseException(String message) {
			super(message);
		}
	}
}
